/*
MIT License

Copyright (c) 2021 МГТУ им. Н.Э. Баумана, кафедра ИУ-6, Михаил Фетисов,

https://bmstu.codes/lsx/simodo
*/

#include "simodo/utility/grammatize.h"
#include "simodo/inout/convert/functions.h"
#include "simodo/inout/format/fmt.h"
#include "simodo/parser/fuze/FuzeRdp.h"
#include "simodo/ast/generator/FormationFlow.h"
#include "simodo/interpret/builtins/hosts/fuze/FuzeAnalyzer.h"
#include "simodo/interpret/builtins/hosts/base/BaseAnalyzer.h"
#include "simodo/interpret/builtins/hosts/base/BaseRunning.h"
#include "simodo/interpret/builtins/modules/LexicalParametersModule.h"
#include "simodo/interpret/builtins/modules/AstFormationModule.h"
#include "simodo/utility/common_functions.h"
#include "simodo/utility/generateDotFile.h"
#include "simodo/loom/Loom.h"
#include "simodo/variable/convert/Ast.h"
#include "simodo/variable/json/Serialization.h"

#include <fstream>
#include <memory>
#include <algorithm>
#include <chrono>

#if __cplusplus >= __cpp_2017
#include <filesystem>
namespace fs = std::filesystem;
#else
#include <experimental/filesystem>
namespace fs = std::filesystem::experimental;
#endif

namespace simodo::utility
{
    class LocalClock
    {
        double & _seconds;
        std::chrono::time_point<std::chrono::steady_clock> _start;

    public:
        LocalClock() = delete;
        LocalClock(double & seconds) : _seconds(seconds)
        {
            _start = std::chrono::steady_clock::now();
        }

        ~LocalClock()
        {
            _seconds = std::chrono::duration<double>(std::chrono::steady_clock::now() - _start).count();
        }
    };

    void printRules(const parser::Grammar & g, const inout::uri_set_t & files, bool need_st_info, std::ostream & out)
    {
        out << inout::fmt("Правила грамматики:").str() << std::endl;
        for(size_t i=0; i < g.rules.size(); ++i) {
            char action = (g.rules[i].reduce_action.branches().empty()) ? ' ' : '*';
            char direction = (g.rules[i].reduce_direction == parser::RuleReduceDirection::Undefined)
                    ? ' ' : ((g.rules[i].reduce_direction == parser::RuleReduceDirection::LeftAssociative)
                                ? '<' : '>');
            out << i << ": " << action << direction << "\t" << inout::toU8(g.rules[i].production) << "\t→ ";

            for(const inout::Lexeme & lex : g.rules[i].pattern)
                out << utility::getMnemonic(lex) << " ";

            out << std::endl;

            if (need_st_info && !g.rules[i].reduce_action.branches().empty())
                utility::printSemanticTree(g.rules[i].reduce_action, files, 0, 2);
        }
    }

    void printStateTransitions(const parser::Grammar & g, std::ostream & out)
    {
        out << inout::fmt("Состояния автомата разбора:").str() << std::endl;
        for(size_t i=0; i < g.states.size(); ++i) {
            out << "State [" << i << "]:" << std::endl;

            const parser::FsmState_t & state = g.states[i];

            for(const parser::FsmStatePosition & p : state) {
                out << (p.is_main ? "    M:" : "     ") << "\t"
                        << inout::toU8(g.rules[p.rule_no].production) << "\t→ ";

                for(size_t j=0; j < g.rules[p.rule_no].pattern.size(); ++j) {
                    if (p.position == j)
                        out << "•";
                    out << utility::getMnemonic(g.rules[p.rule_no].pattern[j]) << " ";
                }
                if (p.position == g.rules[p.rule_no].pattern.size())
                    out << "•";

                if (!p.lookahead.empty()) {
                    out << "\t|";

                    for(const inout::Lexeme & lex : p.lookahead)
                        out << " " << utility::getMnemonic(lex);
                }

                out << "\t\t---- " << ((p.next_state_no > 0) ? "S" : "R") 
                    << ((p.next_state_no > 0) ? p.next_state_no : p.rule_no) 
                    << std::endl;
            }
        }

        out << inout::fmt("Символы грамматики:").str() << std::endl;
        for(size_t i=0; i < g.columns.size(); ++i)
            out << i << ": " << utility::getMnemonic(g.columns[i]) << std::endl;

        out << inout::fmt("Таблица разбора:").str();
        size_t cl         = 100000;
        size_t table_size = 0;
        for(auto [fsm_key,fsm_value] : g.parse_table) {
            size_t        line     = g.unpackFsmState(fsm_key);
            size_t        column   = g.unpackFsmColumn(fsm_key);
            parser::FsmActionType action   = g.unpackFsmAction(fsm_value);
            size_t        location = g.unpackFsmLocation(fsm_value);

            if (cl != line)
                out << std::endl << "[" << line << "]:\t";

            out << utility::getMnemonic(g.columns[column]) 
                 << "(" << inout::toU8(parser::getFsmActionChar(action)) 
                 << location << ") ";

            cl = line;
            table_size ++;
        }
        out << std::endl << inout::fmt("Количество элементов = ").str() << table_size << std::endl;
    }

    void createStateTransitionsGraph(const parser::Grammar & g, 
                                     const std::string & dot_file, 
                                     const std::string & grammar_file, 
                                     bool need_silence,
                                     std::ostream & out)
    {
        std::ofstream dot(dot_file);

        if(dot.good())
        {
            // dot << "digraph \"" << grammar_file << "\" { rankdir=\"LR\";" << endl;
            dot << "digraph \"" << grammar_file << "\" {" << std::endl;
            for(size_t i=0; i < g.states.size(); ++i)
            {
                dot << "\tS" << i << " [shape=none,style=filled,margin=0,fontsize=12,fontname=Helvetica,labelfloat=false,labelloc=t,labeljust=l,label=<" << std::endl
                    << "\t\t<TABLE BORDER=\"1\" CELLBORDER=\"0\" CELLSPACING=\"0\" CELLPADDING=\"2\">" << std::endl
                    << "\t\t<TR><TD COLSPAN=\"2\"><b>" << i << "</b></TD></TR>" << std::endl;

                const parser::FsmState_t & state = g.states[i];

                for(const parser::FsmStatePosition & p : state)
                {

                    dot << "\t\t\t<TR><TD align='right'>" << (p.is_main ? "* " : "") 
                        << utility::toHtml(inout::toU8(g.rules[p.rule_no].production))
                        << " →</TD><TD align='left'>";

                    for(size_t j=0; j < g.rules[p.rule_no].pattern.size(); ++j)
                    {
                        if (p.position == j)
                            dot << "•";
                        dot << utility::toHtml(utility::getMnemonic(g.rules[p.rule_no].pattern[j])) << " ";
                    }
                    if (p.position == g.rules[p.rule_no].pattern.size())
                        dot << "•";

                    if (!p.lookahead.empty())
                    {
                        dot << " |";

                        for(const inout::Lexeme & lex : p.lookahead)
                            dot << " " << utility::toHtml(utility::getMnemonic(lex));
                    }

                    dot << "</TD></TR>" << std::endl;
                }
                dot << "\t\t</TABLE>>];" << std::endl;
            }
            for(size_t i=0; i < g.states.size(); ++i)
            {
                const parser::FsmState_t & state = g.states[i];

                for(size_t ip=0; ip < state.size(); ++ip)
                {
                    const parser::FsmStatePosition & p = state[ip];

                    if (p.next_state_no > 0)
                    {
                        size_t j = 0;
                        for(; j < ip; ++j)
                            if (state[j].position < g.rules[state[j].rule_no].pattern.size() &&
                                    p.position < g.rules[p.rule_no].pattern.size() &&
                                    g.rules[state[j].rule_no].pattern[state[j].position] == g.rules[p.rule_no].pattern[p.position])
                                break;

                        if (j < ip)
                            continue;

                        std::string label;
                        if (p.position < g.rules[p.rule_no].pattern.size())
                            label = utility::toHtml(utility::getMnemonic(g.rules[p.rule_no].pattern[p.position]));
                        dot <<  "\t\tS" << i << " -> " << "S" << p.next_state_no
                                << " [fontsize=12,fontname=Helvetica,label=<" << label << ">];" << std::endl;
                    }
                }
            }
            dot << "}" << std::endl;

            if (dot.good()) {
                if (!need_silence)
                    out << inout::fmt("Создан DOT-файл '").str() << dot_file << "'" << std::endl;
            }
            else
                out << inout::fmt("Не удалось записать в файл '").str() << dot_file << "'" << std::endl;
        }
        else
            out << inout::fmt("Не удалось записать в файл '").str() << dot_file << "'" << std::endl;
    }

    bool grammatize(const std::string & grammar_file,
                inout::InputStream_interface & in,
                std::ostream & out,
                inout::Reporter_abstract & m,
                const std::string & json_file_name,
                const std::string & st_dot_file_name,
                parser::TableBuildMethod grammar_builder_method,
                const std::string & dot_file_name,
                bool need_state_transitions_info,
                bool need_rules_info,
                bool need_st_info,
                bool need_time_intervals,
                bool need_silence,
                bool need_build_grammar,
                bool need_load_grammar,
                bool need_analyze_handles,
                bool need_analyze_inserts,
                parser::Grammar & grammar)
    {
        bool    ok = true;

        if (need_load_grammar) {
            if (!parser::loadGrammarDump(grammar_file,grammar))
                out << inout::fmt("Ошибка при загрузке дампа грамматики '").str() << grammar_file << "'" << std::endl;
            else if (!need_silence) {
                out << inout::fmt("Дамп грамматики успешно загружен").str() << std::endl;
                out << inout::fmt("Грамматика '%1' построена методом %2")
                        .arg(grammar_file)
                        .arg(getGrammarBuilderMethodName(grammar.build_method)).str()
                    << std::endl;
            }
        }
        else {
            double  total_work_time = 0,
                    parse_time = 0,
                    built_time = 0,
                    script_analyze_time = 0;

            {
                LocalClock total_work_clock(total_work_time);

                ast::FormationFlow  flow;
                parser::FuzeRdp     fuze(m, grammar_file, flow);

                {
                    LocalClock  parse_clock(parse_time);
                    ok = fuze.parse(in);
                }

                if (ok) {
                    loom::Loom loom;

                    {
                        LocalClock built_clock(built_time);

                        interpret::Interpret    inter(interpret::InterpretType::Analyzer, m, loom, flow.tree().files(), flow.tree().root(),
                                                {
                                                    new interpret::builtins::FuzeAnalyzer(
                                                            &inter,
                                                            fs::path(grammar_file).stem().string(),
                                                            grammar,
                                                            grammar_builder_method),
                                                });

                        loom.stretch(&inter);
                        loom.finish();

                        grammar.files = flow.tree().files();
                        ok = !inter.errors();
                    }

                    if (ok) {
                        if (!need_silence)
                            out << inout::fmt("Грамматика '%1' построена методом %2")
                                    .arg(grammar_file)
                                    .arg(getGrammarBuilderMethodName(grammar.build_method)).str()
                                << std::endl;

                        {
                            LocalClock script_analyze_clock(script_analyze_time);

                            if (need_build_grammar || need_analyze_handles) 
                                for(const auto & [name, node] : grammar.handlers)
                                {
                                    interpret::Interpret *  inter     = new interpret::Interpret(interpret::InterpretType::Preview, m, loom, flow.tree().files(), node);
                                    interpret::builtins::BaseInterpret_abstract *
                                                        /// \note Используется BaseRunning вместо BaseAnalyzer, чтобы заполнить grammar.lexical
                                                        lex_interpret = new interpret::builtins::BaseRunning(inter);
                                    std::shared_ptr<interpret::builtins::LexicalParametersModule> 
                                                        lex         = std::make_shared<interpret::builtins::LexicalParametersModule>
                                                                            (grammar.lexical);

                                    /// \todo Пересмотреть странную передачу самого себя в свой же метод.
                                    /// PVS Studio: warn V678 An object is used as an argument to its own method.
                                    /// Consider checking the first actual argument of the 'instantiate' function.
                                    lex_interpret->importNamespace(u"lex", lex->instantiate(lex), inout::null_token_location);
                                    inter->instantiateSemantics({lex_interpret});
                                    loom.dock(inter, true);
                                    loom.stretch(inter);
                                    /// \note Возможна гонка за grammar.lexical, поэтому ждём завершения
                                    loom.finish();
                                }
                            
                            if (need_analyze_inserts) {
                                for(const parser::GrammarRule & r : grammar.rules) 
                                {
                                    interpret::Interpret *  inter = new interpret::Interpret(interpret::InterpretType::Analyzer, m, loom, flow.tree().files(), r.reduce_action);
                                    interpret::builtins::BaseAnalyzer *   
                                                        astAnalyzer = new interpret::builtins::BaseAnalyzer(inter);
                                    std::shared_ptr<interpret::builtins::AstFormationModule> 
                                                        ast         = std::make_shared<interpret::builtins::AstFormationModule>();

                                    /// \todo Пересмотреть странную передачу самого себя в свой же метод.
                                    /// PVS Studio: warn V678 An object is used as an argument to its own method.
                                    /// Consider checking the first actual argument of the 'instantiate' function.
                                    astAnalyzer->importNamespace(u"ast", ast->instantiate(ast), inout::null_token_location);
                                    inter->instantiateSemantics({astAnalyzer});
                                    loom.dock(inter, true);
                                    loom.stretch(inter);
                                }

                                loom.finish();
                            }
                        }

                        if (need_build_grammar) {
                            if (!parser::saveGrammarDump(grammar_file,grammar))
                                out << inout::fmt("Ошибка при сохранении дампа грамматики '%1'").arg(grammar_file).str()
                                    << std::endl;
                            else if (!need_silence)
                                out << inout::fmt("Дамп грамматики успешно сохранён").str() 
                                    << std::endl;
                        }

                        if (!st_dot_file_name.empty()) {
                            if (!need_silence)
                                out << inout::fmt("Построение DOT-файла...").str() 
                                    << std::endl;
                            utility::generateDotFile(st_dot_file_name, flow.tree().root(), {});
                        }

                        if (!json_file_name.empty()) {
                            if (!need_silence)
                                out << inout::fmt("Сохранение JSON-файла...").str() 
                                    << std::endl;

                            ok = variable::saveJson(variable::toValue(flow.tree()), json_file_name, false);

                            if (!ok)
                                out << inout::fmt("Ошибка сохранения в '%1'").arg(json_file_name).str() 
                                    << std::endl;
                        }
                    }
                    else
                        out << inout::fmt("При построении грамматики '%1' методом %2 возникли ошибки")
                                .arg(grammar_file)
                                .arg(getGrammarBuilderMethodName(grammar_builder_method)).str()
                            << std::endl;
                }
                else if (!need_silence)
                    out << inout::fmt("Синтаксический анализ описания грамматики '%1' выявил ошибки")
                            .arg(grammar_file).str()
                        << std::endl;
            }

            if (need_time_intervals) {
                out << inout::fmt("Время разбора грамматики:\t%1 ms").arg(parse_time*1000).str()
                    << std::endl;
                out << inout::fmt("Время построения таблиц:\t%1 ms").arg(built_time*1000).str()
                    << std::endl;
                out << inout::fmt("Время анализа скриптов:\t\t%1 ms").arg(script_analyze_time*1000).str()
                    << std::endl;
                out << inout::fmt("Общее время работы с грамматикой:\t%1 ms").arg(total_work_time*1000).str()
                    << std::endl;
            }
        }

        if (need_st_info) {
            out << inout::fmt("Обработчики:").str() << std::endl;
            for(const auto & [handler_name,handler_ast] : grammar.handlers) {
                out << '\t' << inout::toU8(handler_name) << ":" << std::endl;
                utility::printSemanticTree(handler_ast, grammar.files, 0, 2);
            }
        }

        if (need_rules_info)
            printRules(grammar, grammar.files, need_st_info, out);

        if (need_state_transitions_info)
            printStateTransitions(grammar, out);

        if (!dot_file_name.empty())
            createStateTransitionsGraph(grammar, dot_file_name, grammar_file, need_silence, out);

        return ok;
    }
}
